// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.history.integration

import com.intellij.history.ActivityId
import com.intellij.history.core.LocalHistoryFacade
import com.intellij.history.integration.LocalHistoryImpl.Companion.getInstanceImpl
import com.intellij.openapi.Disposable
import com.intellij.openapi.command.CommandEvent
import com.intellij.openapi.command.CommandListener
import com.intellij.openapi.roots.ContentIterator
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.vfs.*
import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.*
import com.intellij.util.SystemProperties
import com.intellij.util.containers.DisposableWrapperList

internal class LocalHistoryEventDispatcher(private val facade: LocalHistoryFacade, private val gateway: IdeaGateway) {
  private val vfsEventListeners = DisposableWrapperList<BulkFileListener>()

  fun startAction() {
    gateway.registerUnsavedDocuments(facade)
    facade.forceBeginChangeSet()
  }

  fun finishAction(name: @NlsContexts.Label String?, activityId: ActivityId?) {
    gateway.registerUnsavedDocuments(facade)
    endChangeSet(name, activityId)
  }

  private fun beginChangeSet() {
    facade.beginChangeSet()
  }

  private fun endChangeSet(name: @NlsContexts.Label String?, activityId: ActivityId?) {
    facade.endChangeSet(name, activityId)
  }

  private fun fileCreated(file: VirtualFile?) {
    if (file == null) return
    beginChangeSet()
    createRecursively(file)
    endChangeSet(null, null)
  }

  /**
   * @return true if the creation was processed
   */
  private fun createRecursivelyUsingWorkspaceTraversal(dir: VirtualFile): Boolean {
    val projectIndexes = IdeaGateway.getVersionedFilterData().myProjectFileIndices
    var containingProjectIndex: ProjectFileIndex? = null
    for (projectIndex in projectIndexes) {
      if (!projectIndex.isInProjectOrExcluded(dir)) continue
      if (containingProjectIndex != null) return false // more than 1 project contains this dir

      containingProjectIndex = projectIndex
    }
    if (containingProjectIndex == null) return false // no project contains this dir

    containingProjectIndex.iterateContentUnderDirectory(dir, ContentIterator { fileOrDir ->
      if (isVersioned(fileOrDir)) {
        facade.created(gateway.getPathOrUrl(fileOrDir), fileOrDir.isDirectory)
      }
      true
    }, VirtualFileFilter { file -> isVersioned(file) })
    return true
  }

  private fun createRecursively(f: VirtualFile) {
    if (USE_WORKSPACE_TRAVERSAL) {
      if (createRecursivelyUsingWorkspaceTraversal(f)) return
    }
    VfsUtilCore.visitChildrenRecursively(f, object : VirtualFileVisitor<Void>() {
      override fun visitFile(f: VirtualFile): Boolean {
        if (isVersioned(f)) {
          facade.created(gateway.getPathOrUrl(f), f.isDirectory)
        }
        return true
      }

      override fun getChildrenIterable(f: VirtualFile): Iterable<VirtualFile> {
        // For unversioned files we try to get cached children in hope that they are already generated by content root manager:
        //  cached children may mean that there are versioned sub-folders or sub-files.
        return if (gateway.isVersioned(f, true)) IdeaGateway.loadAndIterateChildren(f) else IdeaGateway.iterateDBChildren(f)
      }
    })
  }

  private fun beforeContentsChange(e: VFileContentChangeEvent) {
    val f = e.file
    if (!gateway.areContentChangesVersioned(f)) return

    val content = gateway.acquireAndUpdateActualContent(f, null) ?: return
    facade.contentChanged(gateway.getPathOrUrl(f), content.first, content.second)
  }

  private fun handleBeforeEvent(event: VFileEvent) {
    if (event is VFileContentChangeEvent) {
      beforeContentsChange(event)
    }
    else if (event is VFilePropertyChangeEvent && event.isRename || event is VFileMoveEvent) {
      val f = event.file!!
      f.putUserData(WAS_VERSIONED_KEY, gateway.isVersioned(f))
    }
    else if (event is VFileDeleteEvent) {
      beforeFileDeletion(event)
    }
  }

  private fun propertyChanged(e: VFilePropertyChangeEvent) {
    if (e.isRename) {
      val f = e.file

      val isVersioned = gateway.isVersioned(f)
      val wasVersioned = f.getUserData(WAS_VERSIONED_KEY) ?: return
      f.putUserData(WAS_VERSIONED_KEY, null)

      if (!wasVersioned && !isVersioned) return

      val oldName = e.oldValue as String
      facade.renamed(gateway.getPathOrUrl(f), oldName)
    }
    else if (VirtualFile.PROP_WRITABLE == e.propertyName) {
      if (!isVersioned(e.file)) return
      val f = e.file
      if (!f.isDirectory) {
        val oldWritableValue = e.oldValue as Boolean
        facade.readOnlyStatusChanged(gateway.getPathOrUrl(f), !oldWritableValue)
      }
    }
  }

  private fun fileMoved(e: VFileMoveEvent) {
    val f = e.file

    val isVersioned = gateway.isVersioned(f)
    val wasVersioned = f.getUserData(WAS_VERSIONED_KEY) ?: return
    f.putUserData(WAS_VERSIONED_KEY, null)

    if (!wasVersioned && !isVersioned) return

    facade.moved(gateway.getPathOrUrl(f), gateway.getPathOrUrl(e.oldParent))
  }

  private fun beforeFileDeletion(e: VFileDeleteEvent) {
    val f = e.file
    val entry = gateway.createEntryForDeletion(f) ?: return
    facade.deleted(gateway.getPathOrUrl(f), entry)
  }

  private fun isVersioned(f: VirtualFile): Boolean = gateway.isVersioned(f)

  private fun handleBeforeEvents(events: List<VFileEvent>) {
    gateway.runWithVfsEventsDispatchContext(events, true) {
      for (event in events) {
        handleBeforeEvent(event)
      }
      for (listener in vfsEventListeners) {
        listener.before(events)
      }
    }
  }

  private fun handleAfterEvents(events: List<VFileEvent>) {
    gateway.runWithVfsEventsDispatchContext(events, false) {
      for (event in events) {
        handleAfterEvent(event)
      }
      for (listener in vfsEventListeners) {
        listener.after(events)
      }
    }
  }

  private fun handleAfterEvent(event: VFileEvent) {
    when (event) {
      is VFileCreateEvent -> fileCreated(event.getFile())
      is VFileCopyEvent -> fileCreated(event.findCreatedFile())
      is VFilePropertyChangeEvent -> propertyChanged(event)
      is VFileMoveEvent -> fileMoved(event)
    }
  }

  fun addVirtualFileListener(virtualFileListener: BulkFileListener, disposable: Disposable) {
    vfsEventListeners.add(virtualFileListener, disposable)
  }

  internal class LocalHistoryFileManagerListener : VirtualFileManagerListener {
    override fun beforeRefreshStart(asynchronous: Boolean) {
      getInstanceImpl().getEventDispatcher()?.beginChangeSet()
    }

    override fun afterRefreshFinish(asynchronous: Boolean) {
      getInstanceImpl().getEventDispatcher()?.endChangeSet(LocalHistoryBundle.message("activity.name.external.change"), CommonActivity.ExternalChange)
    }
  }

  internal class LocalHistoryCommandListener : CommandListener {
    override fun commandStarted(e: CommandEvent) {
      getInstanceImpl().getEventDispatcher()?.beginChangeSet()
    }

    override fun commandFinished(e: CommandEvent) {
      getInstanceImpl().getEventDispatcher()?.endChangeSet(e.commandName, CommonActivity.Command)
    }
  }

  internal class LocalHistoryBulkFileListener : BulkFileListener {
    override fun before(events: List<VFileEvent>) {
      getInstanceImpl().getEventDispatcher()?.handleBeforeEvents(events)
    }

    override fun after(events: List<VFileEvent>) {
      getInstanceImpl().getEventDispatcher()?.handleAfterEvents(events)
    }
  }

  companion object {
    private val WAS_VERSIONED_KEY = Key.create<Boolean>(LocalHistoryEventDispatcher::class.java.simpleName + ".WAS_VERSIONED_KEY")
    private val USE_WORKSPACE_TRAVERSAL = SystemProperties.getBooleanProperty("lvcs.use-workspace-traversal", true)
  }
}